﻿// <copyright file="Model3D.cs" company="Axelerate Solutions S.A.">
// Copyright (c) Axelerate Solutions S.A.  All rights reserved.
// </copyright>

namespace Axelerate.Silverlight3D.Media.Media3D
{
    using System;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Ink;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;
    using Axelerate.Silverlight3D.Controls;
    using System.Collections.Generic;

    /// <summary>
    /// File name: Model3D.cs
    /// Company: Axelerate Solutions S.A.
    /// Created By: Daniel Rojas
    /// Created Date: 10/07/2008 14:34:00 h.
    /// Description: Abstract class that provides functionality for 3-D models.
    ///   <Modifications>
    ///   </Modifications>
    /// </summary>
    /// <remarks>Objects of this type include GeometryModel3D and Light.</remarks>
    public abstract class Model3D : Freezable, IFormattable
    {
        #region Fields

        /// <summary>
        /// Identifies the Transform dependency property.
        /// </summary>
        public static readonly DependencyProperty TransformProperty =
                    DependencyProperty.Register(
                          "Transform",
                          typeof(Transform3D),
                          typeof(Model3D),
                          new PropertyMetadata(OnTransformPropertyChanged));

        /// <summary>
        /// Keep a copy of the Matrix3D by parent ModelVisual3D already multiply by the parent Matrix Transformation
        /// </summary>
        protected Dictionary<ModelVisual3D, Matrix3D> localToWorldTransforms = new Dictionary<ModelVisual3D, Matrix3D>();

        /// <summary>
        /// Copy of the Matrix3D of the Parent's ModelVisual3D.
        /// </summary>
        protected Dictionary<ModelVisual3D, Matrix3D> parentWorldTransforms = new Dictionary<ModelVisual3D, Matrix3D>();

        #region internal Fields

        /// <summary>
        /// Keep a cache copy of the render entities that represent on 2D this Mesh
        /// </summary>
        internal List<RenderEntity> _cachedRenderEntities = new List<RenderEntity>();

        #endregion

        #endregion
        
        #region Constructors
        protected Model3D()
        {
            this.IsDirty = true;
            this.zIndexOverride = 0;
            this.Transform = new MatrixTransform3D();
        }
        #endregion

        #region Properties

        /// <summary>
        /// Indicates if this model needs to be re-rendered
        /// </summary>
        internal bool IsDirty
        {
            get;
            set;
        }



        public double zIndexOverride
        {
            get { return (double)GetValue(zIndexOverrideProperty); }
            set { SetValue(zIndexOverrideProperty, value); }
        }

        // Using a DependencyProperty as the backing store for zIndexOverride.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty zIndexOverrideProperty =
            DependencyProperty.Register("zIndexOverride", typeof(double), typeof(Model3D), new PropertyMetadata(OnzIndexOverridePropertyChanged));

        /// <summary>
        /// Gets or sets the color allowed to emit from the Material. This is a dependency property.
        /// </summary>
        public Transform3D Transform
        {
            get
            {

                return (Transform3D)this.GetValue(TransformProperty);
            }
            set
            {
                Transform3D OldValue = (Transform3D)this.GetValue(TransformProperty);
                if (OldValue != null)
                {
                    OldValue.PropertyChanged -= TransFormValue_PropertyChanged;
                }
                this.SetValue(TransformProperty, value);
                value.PropertyChanged += TransFormValue_PropertyChanged;
            }
        }

        /// <summary>
        /// Notify if the some property of the Transform was changed
        /// </summary>
        /// <param name="sender">Object that send the event</param>
        /// <param name="e">event arguments</param>
        void TransFormValue_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            this.IsDirty = true;
            this.NotifyPropertyChanged("Transform");
            this.UpdateWorldTransform();
        }

        /// <summary>
        /// Gets the bounding Rect3D for this Model3D. (not implemented yet)
        /// </summary>
        public Rect3D Bounds
        {
            get
            {
                return this.GetBounds();
            }
        }

        
        #endregion
        
        #region Methods


        /// <summary>
        /// When overrited gets the bounding Rect3D for this Model3D.
        /// </summary>
        protected abstract Rect3D GetBounds();

        /// <summary>
        /// Raise the Transform changed event
        /// </summary>
        /// <param name="d">DependencyObject owner of the property</param>
        /// <param name="e">DependencyPropertyChangedEventArgs with the necesary information</param>
        private static void OnTransformPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((Model3D)d).NotifyPropertyChanged("Transform");
            ((Model3D)d).IsDirty = true;
            ((Model3D)d).UpdateWorldTransform();
        }
        /// <summary>
        /// Raise the zIndexOverride changed event
        /// </summary>
        /// <param name="d">DependencyObject owner of the property</param>
        /// <param name="e">DependencyPropertyChangedEventArgs with the necesary information</param>
        private static void OnzIndexOverridePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((Model3D)d).NotifyPropertyChanged("zIndexOverride");
            ((Model3D)d).IsDirty = true;
        }

        internal void SetParentWorldTransform(ModelVisual3D visual, Matrix3D transform)
        {
            this.parentWorldTransforms[visual] = transform;
            this.UpdateWorldTransform();
        }

        /// <summary>
        /// Returns the LocalToWorld transform for the model which is associated
        /// with the visual
        /// </summary>
        /// <param name="visual"></param>
        /// <returns></returns>
        internal Matrix3D GetLocalToWorldTransform(ModelVisual3D visual)
        {
            if (!this.localToWorldTransforms.ContainsKey(visual))
            {
                return this.Transform.Value;
            }
            return this.localToWorldTransforms[visual];
        }

        /// <summary>
        /// Update the matrix using the parent matrix's
        /// </summary>
        internal virtual void UpdateWorldTransform()
        {
            foreach (ModelVisual3D modelVisual in this.parentWorldTransforms.Keys)
            {
                this.localToWorldTransforms[modelVisual] = this.Transform.Value * this.parentWorldTransforms[modelVisual];
            }
            this.IsDirty = true;
        }

        /// <summary>
        /// Abstract method that is used to re-create the render entities on the models.
        /// </summary>
        /// <param name="entityPool">viewport entitypool to avoid create new renderentites every time.</param>
        internal abstract void CreateRenderEntities(RenderEntityPool entityPool);

        /// <summary>
        /// Performe the projection of the renderenties caculating color and posision on the respective viewport.
        /// </summary>
        /// <param name="Parent">parent model visual of this model. The model visual is who call the render.</param>
        /// <param name="Viewport">Viewport where the render entities will be added.</param>
        /// <param name="entitiesToRender">list of render entities that will be added to the viewport for render</param>
        /// <param name="worldToScreenMatrix">Matrix of the camera that can be used to translate from world coordinates to screen coordinates</param>
        /// <param name="lights">list of ligths used to calculate the color of the shapes</param>
        /// <returns>returns a value indicating if the model was rendered</returns>
        internal virtual bool Render(ModelVisual3D Parent, Viewport3D Viewport, List<RenderEntity> entitiesToRender, Matrix3D worldToScreenMatrix, List<Light> lights)
        {
            List<RenderEntity> allEntities = this._cachedRenderEntities;
            Material CurrentMaterial = null;
            Matrix3D worldMatrix = this.GetLocalToWorldTransform(Parent);
            bool modelWasRendered = false;
            Camera camera = Viewport.Camera;

            if (!Viewport.IsViewportDirty && !this.IsDirty)
            {
                #region if not need to reproyect the entities
                entitiesToRender.AddRange(allEntities);
                modelWasRendered = true;

                // If lights is dirty we require to reset the color of the entities acording with the lights
                if (Viewport.IsLightsDirty && Viewport.LightModel != LightModelEnum.None)
                {
                    foreach (RenderEntity entity in allEntities)
                    {
                        if (Viewport.LightModel == LightModelEnum.Flat)
                        {
                            Vector3D entityNormal = entity.Normal;
                            if (entity.IsBackFacing)
                            {
                                entityNormal.Negate();
                            }
                            if (entity.Visual is Shape)
                            {
                                (entity.Visual as Shape).Stroke = (entity.Visual as Shape).Fill = CurrentMaterial.GetBrush(lights, worldMatrix.Transform(entityNormal), ((ProjectionCamera)camera).LookDirection, false);
                                (entity.Visual as Shape).StrokeEndLineCap = PenLineCap.Flat;
                                (entity.Visual as Shape).StrokeThickness = 0.5;
                                (entity.Visual as Shape).StrokeStartLineCap = PenLineCap.Flat;
                            }
                        }
                        else
                        {
                            if (entity.Visual is Shape)
                            {
                                (entity.Visual as Shape).Stroke = (entity.Visual as Shape).Fill = ((DiffuseMaterial)CurrentMaterial).Brush;
                                (entity.Visual as Shape).StrokeEndLineCap = PenLineCap.Flat;
                                (entity.Visual as Shape).StrokeThickness = 0.5;
                                (entity.Visual as Shape).StrokeStartLineCap = PenLineCap.Flat;
                            }
                        }
                    }
                }
                #endregion
            }
            else
            {
                #region if must do the proyection
                // calculate the local to screen matrix
                Matrix3D localToScreenMatrix = worldMatrix * worldToScreenMatrix;
                
                // Create a cache for the projected Point3D to aviod recalculate it if allready was projected
                Dictionary<Point3D, Point3D> ProjectedPointCache = new Dictionary<Point3D, Point3D>(1024);
                bool entitiesRendered = false;
                #region entity render
                foreach (RenderEntity entity in allEntities)
                {
                    entity.ZIndexOverride = this.zIndexOverride;
                    if (entity.IsTriangle)
                    {
                        Point3D p0;
                        if (ProjectedPointCache.ContainsKey(entity.P0))
                        {
                            p0 = ProjectedPointCache[entity.P0];
                        }
                        else
                        {
                            p0 = localToScreenMatrix.Transform(entity.P0);
                        }

                        Point3D p1;
                        if (ProjectedPointCache.ContainsKey(entity.P1))
                        {
                            p1 = ProjectedPointCache[entity.P1];
                        }
                        else
                        {
                            p1 = localToScreenMatrix.Transform(entity.P1);
                        }
                        Point3D p2;
                        if (ProjectedPointCache.ContainsKey(entity.P2))
                        {
                            p2 = ProjectedPointCache[entity.P2];
                        }
                        else
                        {
                            p2 = localToScreenMatrix.Transform(entity.P2);
                        }

                        // Cull the back facing entities
                        entity.IsBackFacing = Vector3D.CrossProduct(p1 - p0, p2 - p0).Z >= 0;

                        if ((entity.BackMaterial == null) && entity.IsBackFacing)
                        {
                            // The entity does not have a back material
                            // so we will not render the entity
                            continue;
                        }
                        else if (entity.BackMaterial != null && entity.IsBackFacing)
                        {
                            // Use the back material
                            CurrentMaterial = entity.BackMaterial;
                        }
                        else
                        {
                            // Use the front material
                            CurrentMaterial = entity.FrontMaterial;
                        }


                        if (Viewport.SeamSmoothing != 0)
                        {
                            Point3D screenTriangleCentroid = new Point3D((p0.X + p1.X + p2.X) / 3,
                                                                         (p0.Y + p1.Y + p2.Y) / 3,
                                                                         (p0.Z + p1.Z + p2.Z) / 3);

                            Vector3D p0FromCenter = p0 - screenTriangleCentroid;
                            p0FromCenter.Normalize();
                            Vector3D p1FromCenter = p1 - screenTriangleCentroid;
                            p1FromCenter.Normalize();
                            Vector3D p2FromCenter = p2 - screenTriangleCentroid;
                            p2FromCenter.Normalize();

                            // Define how much we should expand each entity vertice
                            p0 = p0 + p0FromCenter * Viewport.SeamSmoothing;
                            p1 = p1 + p1FromCenter * Viewport.SeamSmoothing;
                            p2 = p2 + p2FromCenter * Viewport.SeamSmoothing;
                        }

                        Point3D entityCentroidInWorldCoords;
                        if (ProjectedPointCache.ContainsKey(entity.Centroid))
                        {
                            entityCentroidInWorldCoords = ProjectedPointCache[entity.Centroid];
                        }
                        else
                        {
                            entityCentroidInWorldCoords = worldMatrix.Transform(entity.Centroid);
                        }
                        Vector3D x = (entityCentroidInWorldCoords - ((PerspectiveCamera)Viewport.Camera).Position);

                        // Distance of the entity from the camera
                        entity.ZIndex = x.LengthSquared;

                        // TODO: extend this for support also rectangles for the render entities
                        entity.SetTrianglePoints(new Point(p0.X, p0.Y),
                                                 new Point(p1.X, p1.Y),
                                                 new Point(p2.X, p2.Y));

                        // depending of the lighting model is necesary to recalculate the color of the render entity.
                        if (Viewport.LightModel == LightModelEnum.Flat)
                        {
                            Vector3D entityNormal = entity.Normal;
                            if (entity.IsBackFacing)
                            {
                                entityNormal.Negate();
                            }
                            if (entity.Visual is Shape)
                            {
                                (entity.Visual as Shape).Stroke = (entity.Visual as Shape).Fill = CurrentMaterial.GetBrush(lights, worldMatrix.Transform(entityNormal), ((ProjectionCamera)camera).LookDirection, false);
                                (entity.Visual as Shape).StrokeEndLineCap = PenLineCap.Flat;
                                (entity.Visual as Shape).StrokeThickness = 0.5;
                                (entity.Visual as Shape).StrokeStartLineCap = PenLineCap.Flat;
                            }
                        }
                        else
                        {
                            if (entity.Visual is Shape)
                            {
                                (entity.Visual as Shape).Stroke = (entity.Visual as Shape).Fill = ((DiffuseMaterial)CurrentMaterial).Brush;
                                (entity.Visual as Shape).StrokeEndLineCap = PenLineCap.Flat;
                                (entity.Visual as Shape).StrokeThickness = 0.5;
                                (entity.Visual as Shape).StrokeStartLineCap = PenLineCap.Flat;
                            }
                        }
                    }
                    else
                    {
                        // this if for sprites.
                        Point3D p0;
                        if (ProjectedPointCache.ContainsKey(entity.Centroid))
                        {
                            p0 = ProjectedPointCache[entity.Centroid];
                        }
                        else
                        {
                            p0 = localToScreenMatrix.Transform(entity.Centroid);
                        }

                        Point3D entityCentroidInWorldCoords = worldMatrix.Transform(entity.Centroid);

                        Vector3D x = (entityCentroidInWorldCoords - ((PerspectiveCamera)Viewport.Camera).Position);

                        // Distance of the entity from the camera
                        entity.ZIndex = x.LengthSquared;

                        Canvas.SetLeft(entity.Visual, p0.X);
                        Canvas.SetTop(entity.Visual, p0.Y);
                    }

                    entitiesToRender.Add(entity);
                    // Indicate that some entities were rendered
                    entitiesRendered = true;
                }
                #endregion

                if (entitiesRendered)
                {
                    modelWasRendered = true;
                }
                #endregion
            }
            if (modelWasRendered)
            {
                Parent.RenderedModels.Add(this);
            }
            return modelWasRendered;
        }
        #endregion

        #region IFormattable Members

        public string ToString(string format, IFormatProvider formatProvider)
        {
            return this.ToString();
        }

        #endregion
    }
}
